package scene;

import java.util.ArrayList;
import java.util.List;

import raytracing.AccelerationStructure;
import raytracing.Camera;
import raytracing.HitRecord;
import raytracing.Ray;
import scene.accel.bvh.BVHNode;
import scene.lighting.AreaLight;
import scene.lighting.Light;
//import scene.shape.BoundingBox;
import scene.shape.Shape;
import scene.shape.geometry.Primitive;
import scene.shape.geometry.Triangle;
import scene.shape.materials.Material;
import util.Color3f;
import util.Point2f;
import util.Vector3f;

public class Scene {
	
	private List<Camera> cameras;
	private List<Shape> shapes;
	private List<Light> lights;
	
	private Camera currentCamera;
	private Color3f backgroundColor;
	
	private List<Primitive> infinitePrimitives;
	public AccelerationStructure accel;
	private CubeMap cubeMap = null;
	//private BoundingBox worldBound;               
	private static Scene scene;
	public static final float EPSILON = 0.001f;
	
	public static Scene getInstance(){
		if(scene != null)
			return scene;
		scene = new Scene();
		return scene;
	}
	
	public CubeMap getCubeMap(){
		return cubeMap;
	}
	
	private Scene(){
		cameras = new ArrayList<Camera>();
		shapes = new ArrayList<Shape>();
		lights = new ArrayList<Light>();
		this.backgroundColor = new Color3f(0,0,0);
	}
	
	public void addCamera(Camera camera) {
		cameras.add(camera);
	}
	
	public void addShape(Shape s){
		shapes.add(s);
	}
	
	public void setCurrentCamera(Camera camera){
		this.currentCamera = camera;
	}
	
	public Camera getCurrentCamera(){
		return currentCamera;
	}
	
	public List<Camera> getCameras(){
		return cameras;
	}

	public List<Shape> getShapes(){
		return shapes;
	}
	
	public List<Light> getLights(){
		return lights;
	}
	
	public Color3f getBackgroundColor(){
		return backgroundColor;
	}
	
	public HitRecord hitScene(Ray r, float mint, float maxt){
		HitRecord closestHit =  accel.intersects(r, mint, maxt);
		for(Primitive p : infinitePrimitives){
			if(closestHit.hasHit())
				maxt = closestHit.getT();
			HitRecord unboundedHit = p.intersects(r, mint, maxt);
			float unboundedT = unboundedHit.getT();
			if(unboundedHit.hasHit() && unboundedT > mint &&  unboundedT < maxt
					&& (!closestHit.hasHit() || unboundedHit.getT() < closestHit.getT()))
				closestHit = unboundedHit;
		}
		return closestHit;
	}

	public void addLight(Light l) {
		lights.add(l);
	}

	public void setBackgroundColor(Color3f background) {
		this.backgroundColor = background;
	}
	public List<Primitive> getPrimitives(){
		List<Primitive> primitives = new ArrayList<Primitive>();
		for(Shape s : shapes){
			primitives.addAll(s.getGeometry().getPrimitiveList());
		}
		return primitives;
	}
	
//	public void buildGrid(){
//		accel = new UniformGrid(this);
//	}
	
	public void buildAccel(){
		//worldBound = BoundingBox.computeBoundingBox(getPrimitives());
//		accel = new UniformGrid(scene);
//		accel.build(scene);
		accel = new BVHNode(getPrimitives(), 0);
		infinitePrimitives = getInfinitePrimitives();
	}

	private List<Primitive> getInfinitePrimitives() {
		List<Primitive> infprims = new ArrayList<Primitive>();
		for(Primitive p : getPrimitives()){
			if(p.getBoundingBox() == null)
				infprims.add(p);
		}
		return infprims;
	}

	public void setCubeMap(CubeMap cubeMap) {
		this.cubeMap = cubeMap;
	}
	
	public Color3f color(Ray r, float mint, float maxt, int recursiondepth){
		HitRecord closestHit = null;
		Color3f c = new Color3f(0,0,0);
		closestHit = scene.hitScene(r, mint, maxt);
		if(closestHit != null && closestHit.hasHit()){
			Material m = closestHit.getPrimitive().getShape().getMaterial();
			if(!m.isLightingDependent())
				return m.shade(closestHit, recursiondepth);
			else{
				for(Light light : Scene.getInstance().getLights()){
					Vector3f l = light.getPosition().subtract(closestHit.getIntersectionPoint()).normalize();
					Ray shadowRay = new Ray(closestHit.getIntersectionPoint(), closestHit.getIntersectionPoint().add(l));
					HitRecord shadowHit = Scene.getInstance().hitScene(shadowRay, EPSILON, Float.POSITIVE_INFINITY);
					if(!shadowHit.hasHit())
						c = c.add(m.shade(closestHit,light, recursiondepth));
					else
						c = c.add(m.getAmbientShading());
				}
			}
		}else{
				if(scene.getCubeMap() == null)
					c = scene.getBackgroundColor();
				else
					c = scene.getCubeMap().getColor(r.getDirection());
		}
		return c;
	}
	
	public Color3f color(Ray r, float mint, float maxt, int recursiondepth, Point2f shadowSample){
		HitRecord closestHit = null;
		Color3f c = new Color3f(0,0,0);
		closestHit = scene.hitScene(r, mint, maxt);
		if(closestHit != null && closestHit.hasHit()){
			Material m = closestHit.getPrimitive().getShape().getMaterial();
			if(!m.isLightingDependent())
				return m.shade(closestHit, recursiondepth);
			else{
				for(Light light : Scene.getInstance().getLights()){
					Vector3f l = null;
					if(light instanceof AreaLight){
						AreaLight areaLight = (AreaLight)light;
						Vector3f pointOnLight = areaLight.getRandomPoint(shadowSample.x, shadowSample.y);
						l = pointOnLight.subtract(closestHit.getIntersectionPoint()).normalize();
					}
					else
						l = light.getPosition().subtract(closestHit.getIntersectionPoint()).normalize();
					Ray shadowRay = new Ray(closestHit.getIntersectionPoint(), closestHit.getIntersectionPoint().add(l));
					HitRecord shadowHit = Scene.getInstance().hitScene(shadowRay, EPSILON, Float.POSITIVE_INFINITY);
					if(!shadowHit.hasHit())
						c = c.add(m.shade(closestHit,light, recursiondepth));
					else
						c = c.add(m.getAmbientShading());
				}
			}
		}else{
				if(scene.getCubeMap() == null)
					c = scene.getBackgroundColor();
				else
					c = scene.getCubeMap().getColor(r.getDirection());
		}
		return c;
	}

	public List<Triangle> getTriangles() {
		List<Triangle> triangles = new ArrayList<Triangle>();
		for(Shape s : shapes){
			for(Primitive primitive : s.getGeometry().getPrimitiveList()){
				if(primitive instanceof Triangle)
					triangles.add((Triangle)primitive);
			}
		}
		return triangles;
	}
}
